﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Threading;
using Ionic.Utils.Zip;

namespace SilverLightStreamingInterface
{
    /// <summary>
    /// Mange interactions with the SL website.
    /// </summary>
    public class SLLiveInterface
    {
        private string _id;
        private string _key;
        private string _service;

        /// <summary>
        /// How long to time out before we should go-away?
        /// </summary>
        public int MinutesToTimeOut { get; set; }

        /// <summary>
        /// We may have to generate some small ZIP files. Where can we write them?
        /// </summary>
        public DirectoryInfo TempDirForZipFiles { get; set; }

        /// <summary>
        /// Access to SL service. Store key and ID - which is how we get in!
        /// </summary>
        /// <param name="ID"></param>
        /// <param name="key"></param>
        public SLLiveInterface(string ID, string key)
        {
            _id = ID;
            _key = key;
            _service = "https://silverlight.services.live.com/";
            MinutesToTimeOut = 120;

            TempDirForZipFiles = new DirectoryInfo(Path.GetTempPath() + "\\SLiveTempDir");

            ///
            /// We are using the parallel library to upload files. Not CPU intensive. Of course,
            /// the scheduler is coded to work on CPU usage, not network bandwidth - so we
            /// are going to game the system when we run. Just set it to 6 at once. That should
            /// be more than enough even on a system with a good high-speed connection.
            /// 

            //TaskManagerPolicy plc = new TaskManagerPolicy(TaskManager.Current.Policy.MinProcessors, 12);
            //_taskManager = TaskManager.Current; // new TaskManager(plc);
        }

        /// <summary>
        /// See if a parciulat app exists.
        /// </summary>
        /// <param name="appName"></param>
        /// <returns></returns>
        public bool PackageExists(string appName)
        {
            var packageNames = GetPackageList();
            return packageNames.Contains(appName);
        }

        /// <summary>
        /// Returns a list of all packages.
        /// </summary>
        /// <returns></returns>
        private string[] GetPackageList()
        {
            var response = DoGet("?max-items=100");
            var items = from fs in response.Elements("fileSet")
                        where (string)fs.Attribute("isApp") == "true"
                        select fs.Attribute("name").Value;
            return items.ToArray();
        }

        /// <summary>
        /// Get some data from the live streaming service.
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        private XElement DoGet(string getRequest)
        {
            string url = _service + _id + getRequest;
            var httpreq = GenerateRequest(url);

            string xmlResponse = null;
            using (WebResponse resp = httpreq.GetResponse())
            {
                Stream strm = resp.GetResponseStream();
                StreamReader rdr = new StreamReader(strm);
                xmlResponse = rdr.ReadToEnd();
                rdr.Close();
            }

            XElement result = XElement.Parse(xmlResponse);
            return result;
        }

        /// <summary>
        /// Set up the request, with credentials.
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        private WebRequest GenerateRequest(string url)
        {
            WebRequest result = HttpWebRequest.Create(url);
            result.Credentials = new NetworkCredential(_id, _key);
            return result;
        }

        /// <summary>
        /// Create a new package.
        /// </summary>
        /// <param name="p"></param>
        public void CreatePackage(string package_name)
        {
            DoMKCOL("/" + package_name);
        }

        /// <summary>
        /// Make new column on the server.
        /// </summary>
        /// <param name="restOfReq"></param>
        private void DoMKCOL(string restOfReq)
        {
            string url = _service + _id + restOfReq;
            var httpreq = GenerateRequest(url);
            httpreq.Method = "MKCOL";

            string xmlResponse = null;
            using (WebResponse resp = httpreq.GetResponse())
            {
                Stream strm = resp.GetResponseStream();
                StreamReader rdr = new StreamReader(strm);
                xmlResponse = rdr.ReadToEnd();
                rdr.Close();
            }
        }
        
        /// <summary>
        /// Remove a file. Sweet!
        /// </summary>
        /// <param name="p"></param>
        public void RemovePackage(string p)
        {
            DoDelete("/" + p);
        }

        /// <summary>
        /// Send a delete off.
        /// </summary>
        /// <param name="p"></param>
        private void DoDelete(string delRequest)
        {
            string url = _service + _id + delRequest;
            var httpreq = GenerateRequest(url);
            httpreq.Method = "DELETE";

            string xmlResponse = null;
            using (WebResponse resp = httpreq.GetResponse())
            {
                Stream strm = resp.GetResponseStream();
                StreamReader rdr = new StreamReader(strm);
                xmlResponse = rdr.ReadToEnd();
                rdr.Close();
            }
        }

        /// <summary>
        /// Upload a package.
        /// </summary>
        /// <param name="pack"></param>
        public void UploadPackageZIP(SLPackage pack)
        {
            if (PackageExists(pack))
            {
                RemovePackage(pack);
            }
            DoPost("/" + pack.Name, pack.ZIPFile);
        }

        public void RemovePackage(SLPackage pack)
        {
            RemovePackage(pack.Name);
        }

        /// <summary>
        /// Post a big file up there
        /// </summary>
        /// <param name="p"></param>
        /// <param name="fileInfo"></param>
        private void DoPost(string postDir, FileInfo fileInfo)
        {
            string url = _service + _id + postDir;
            var httpreq = GenerateRequest(url);
            httpreq.Method = WebRequestMethods.Http.Post;
            //httpreq.Timeout = 1000 * 60 * MinutesToTimeOut;
            httpreq.ContentType = "application/zip";

            ///
            /// Open up the file stream so we are reading in the file
            /// 

            using (BinaryReader br = new BinaryReader(fileInfo.Open(FileMode.Open, FileAccess.Read)))
            {
                //httpreq.ContentLength = fileInfo.Length;

                byte[] all = br.ReadBytes((int)fileInfo.Length);
                httpreq.ContentLength = all.Length;
                Stream requestStream = httpreq.GetRequestStream();
                {
                    //requestStream.WriteTimeout = 1000 * 60 * MinutesToTimeOut;
                    requestStream.Write(all, 0, all.Length);
#if false
                    bool done = false;
                    while (!done)
                    {
                        byte[] readBytes = br.ReadBytes(1024 * 20);
                        if (readBytes.Length == 0)
                        {
                            done = true;
                        }
                        else
                        {
                            requestStream.Write(readBytes, 0, readBytes.Length);
                        }
                    }
#endif
                    requestStream.Close();
                }
                br.Close();
                var resp = httpreq.GetResponse();
            }
        }

        /// <summary>
        /// Create a new Silver Light Streaming Package.
        /// </summary>
        /// <param name="pack"></param>
        public void CreatePackage(SLPackage pack)
        {
            CreatePackage(pack.Name);
        }

        /// <summary>
        /// See if the streaming package exists.
        /// </summary>
        /// <param name="pack"></param>
        /// <returns></returns>
        public bool PackageExists(SLPackage pack)
        {
            return PackageExists(pack.Name);
        }

        /// <summary>
        /// Callback when a file is uploaded. Will be single threaded. But make it quick, otherwise
        /// you could hold up the upload engine.
        /// </summary>
        /// <param name="filename"></param>
        public delegate void FileUploadedCallback(string filename, bool success);

        private FileUploadStateCache _fc = new FileUploadStateCache();

        /// <summary>
        /// Upload the files found in pack, one by one. Only do the ones
        /// that are newer than the last time they were uploaded.
        /// </summary>
        /// <param name="pack"></param>
        /// <param name="cb"></param>
        public void UploadPackage(SLPackage pack, FileUploadedCallback cb)
        {
            ///
            /// Move through each directory in the package, and upload one-by-one.
            /// Parallelize only in a single directory -- this is so we don't end up
            /// loading in the cache for more than one directory at a time. That is
            /// because I've seen some directories as large as 100,000 files for large
            /// conferences.
            /// 

            var dinfo = new AllDirectoryIterator(pack.PackageDirectory);
            foreach (DirectoryInfo d in dinfo)
            {
                var dirCache = _fc.GetCacheForDirectory(d);
                var files = from f in d.GetFiles()
                            where dirCache.TimeOfMark(f) < f.LastWriteTime
                            select f;

                if (pack.PackageDirectory != d && !SubDirExists(pack, d))
                {
                    CreateDirectory(pack, d);
                }
#if true
                foreach (var outOfDateFile in files)
                {
                    try
                    {
                        UploadSingleFile(pack, outOfDateFile);
                        dirCache.MarkFile(outOfDateFile);
                        lock (this)
                        {
                            cb(outOfDateFile.FullName, true);
                        }
                    }
                    catch (Exception)
                    {
                        lock (this)
                        {
                            cb(outOfDateFile.FullName, false);
                        }
                    }
                }
#else
                    Parallel.ForEach(files, () => this, (outOfDateFile, i, local) =>
                        {
                            try
                            {
                                UploadSingleFile(pack, outOfDateFile);
                                dirCache.MarkFile(outOfDateFile);
                                lock (this)
                                {
                                    cb(outOfDateFile.FullName, true);
                                }
                            }
                            catch (Exception e)
                            {
                                lock (this)
                                {
                                    cb(outOfDateFile.FullName, false);
                                }
                            }
                        }, (sl) => { }, _taskManager, TaskCreationOptions.None);
                }
                catch (AggregateException ae)
                {
                    if (ae.InnerExceptions.Count == 1)
                    {
                        throw ae.InnerExceptions[0];
                    }
                    throw ae;
                }
#endif
            }
        }

        /// <summary>
        /// Create a directory for storage!
        /// </summary>
        /// <param name="pack"></param>
        /// <param name="d"></param>
        public void CreateDirectory(SLPackage pack, DirectoryInfo d)
        {
            string relativePath = d.FullName.Substring(pack.PackageDirectory.FullName.Length + 1);
            CreateDirectory(pack, relativePath);
        }

        /// <summary>
        /// Create a package directory
        /// </summary>
        /// <param name="pack"></param>
        /// <param name="relativePath"></param>
        private void CreateDirectory(SLPackage pack, string relativePath)
        {
            string url = _service + _id + "/" + pack.Name + "/" + relativePath;
            var httpreq = GenerateRequest(url);
            httpreq.Method = WebRequestMethods.Http.MkCol;

            var response = httpreq.GetResponse();

        }

        /// <summary>
        /// Iterator to help us flatten all the various directories out there.
        /// </summary>
        private class AllDirectoryIterator : IEnumerable<DirectoryInfo>
        {
            DirectoryInfo _base;
            public AllDirectoryIterator(DirectoryInfo based)
            {
                _base = based;
            }
            public System.Collections.Generic.IEnumerator<DirectoryInfo> GetEnumerator()
            {
                yield return _base;

                foreach (var subd in _base.GetDirectories())
                {
                    foreach (var actualsubd in new AllDirectoryIterator(subd))
                    {
                        yield return actualsubd;
                    }
                }
            }

            #region IEnumerable Members

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

            #endregion
        }

        /// <summary>
        /// Push a single file up to the service.
        /// </summary>
        /// <param name="pack"></param>
        /// <param name="outOfDateFile"></param>
        public void UploadSingleFile(SLPackage pack, FileInfo onefile)
        {
            ///
            /// If this isn't a valid file, quietly fail.
            /// 

            if (!Settings.ValidFileTypes.Contains(onefile.Extension.Substring(1)))
            {
                return;
            }

            ///
            /// Relative path of this file.
            /// 

            string relativepath = onefile.FullName.Substring(pack.PackageDirectory.FullName.Length + 1);

            ///
            /// Do a put, but only get the header
            /// 

            PutAFile(pack.Name + "/" + relativepath.Replace('\\', '/'), onefile);
        }

        /// <summary>
        /// Put a file up on the server.
        /// </summary>
        /// <param name="relativeUrl"></param>
        /// <param name="onefile"></param>
        private void PutAFile(string relativeUrl, FileInfo onefile)
        {
            string url = _service + _id + "/" + relativeUrl;
            var httpreq = GenerateRequest(url);
            httpreq.Method = WebRequestMethods.Http.Put;

            using (BinaryReader rd = new BinaryReader(onefile.Open(FileMode.Open, FileAccess.Read)))
            {
                byte[] data = rd.ReadBytes((int) onefile.Length);
                rd.Close();

                httpreq.ContentType = Settings.MimeType(onefile.Extension.Substring(1));
                httpreq.ContentLength = data.Length;

                using (Stream sending = httpreq.GetRequestStream())
                {
                    sending.Write(data, 0, data.Length);
                    sending.Close();
                    var resp = httpreq.GetResponse();
                }

            }
        }

        /// <summary>
        /// Check to see if a file exists. The file is located on disk locally - we look at the relative path.
        /// </summary>
        /// <param name="pack"></param>
        /// <param name="onefile"></param>
        /// <returns></returns>
        public bool FileExists(SLPackage pack, FileInfo onefile)
        {
            string relativepath = onefile.FullName.Substring(pack.PackageDirectory.FullName.Length+1);
            return FileExists(pack, relativepath);
        }

        /// <summary>
        /// True if this file exists up on SL streaming.
        /// </summary>
        /// <param name="pack"></param>
        /// <param name="relativepath"></param>
        /// <returns></returns>
        private bool FileExists(SLPackage pack, string relativepath)
        {
            try
            {
                ///
                /// Do a get, but only get the header
                /// 

                string url = _service + _id + "/" + pack.Name + "/" + relativepath;
                var httpreq = GenerateRequest(url);
                httpreq.Method = WebRequestMethods.Http.Head;
                WebResponse res = httpreq.GetResponse();
                return true;
            }
            catch (WebException)
            {
            }
            return false;
        }

        /// <summary>
        /// Check to see if a particular subdirectory exists
        /// </summary>
        /// <param name="pack"></param>
        /// <param name="subdir"></param>
        /// <returns></returns>
        public bool SubDirExists(SLPackage pack, DirectoryInfo subdir)
        {
            string relativeFile = subdir.FullName.Substring(pack.PackageDirectory.FullName.Length + 1);
            return SubDirExists(pack, relativeFile);
        }

        /// <summary>
        /// Actually see if the subdir exsits.
        /// </summary>
        /// <param name="pack"></param>
        /// <param name="relativeFile"></param>
        /// <returns></returns>
        private bool SubDirExists(SLPackage pack, string relativeFile)
        {
            try
            {
                ///
                /// Do a get, but only get the header
                /// 

                string url = _service + _id + "/" + pack.Name + "/" + relativeFile;
                var httpreq = GenerateRequest(url);
                httpreq.Method = WebRequestMethods.Http.Head;
                WebResponse res = httpreq.GetResponse();
                return true;
            }
            catch (WebException e)
            {
                if (e.Message.Contains("Method Not Allowed"))
                {
                    return true;
                }
                return false;
            }
        }

        /// <summary>
        /// Create a provisioned app by uploading a manifest file as a ZIP file. Nothing else. :-)
        /// </summary>
        /// <param name="pkgName"></param>
        /// <param name="manifestFile"></param>
        public void CreateProvisionedPackage(string pkgName, FileInfo manifestFile)
        {
            ///
            /// Turn the manifest file into a zip file
            /// 

            if (!TempDirForZipFiles.Exists)
            {
                TempDirForZipFiles.Create();
            }
            FileInfo outZip = new FileInfo(TempDirForZipFiles.FullName + "\\" + pkgName + "-manifest.zip");
            if (outZip.Exists)
            {
                outZip.Delete();
            }

            string currentWorkingDir = null;
            try
            {
                currentWorkingDir = Environment.CurrentDirectory;
                Environment.CurrentDirectory = manifestFile.Directory.FullName;
                using (ZipFile outzip = new ZipFile(outZip.FullName))
                {
                    outzip.AddFile(manifestFile.Name);
                    outzip.Save();
                }
            }
            finally
            {
                if (currentWorkingDir != null)
                {
                    Environment.CurrentDirectory = currentWorkingDir;
                }
            }

            ///
            /// Ok -- upload the ZIP file now.
            /// 

            outZip.Refresh();
            SLPackage p = new SLPackage(pkgName) { ZIPFile = outZip };
            UploadPackageZIP(p);
        }
    }
}
